{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cifar-10 testset classification on Pynq\n",
    "\n",
    "This notebook covers how to use low quantized neural networks on Pynq. \n",
    "It shows an example how CIFAR-10 testset can be inferred utilizing different precision neural networks inspired at VGG-16, featuring 6 convolutional layers, 3 max pool layers and 3 fully connected layers. There are 3 different precision available:\n",
    "\n",
    "- CNVW1A1 using 1 bit weights and 1 bit activation,\n",
    "- CNVW1A2 using 1 bit weights and 2 bit activation and\n",
    "- CNVW2A2 using 2 bit weights and 2 bit activation"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Import the package"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/javascript": [
       "\n",
       "require(['notebook/js/codecell'], function(codecell) {\n",
       "  codecell.CodeCell.options_default.highlight_modes[\n",
       "      'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n",
       "  Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n",
       "      Jupyter.notebook.get_cells().map(function(cell){\n",
       "          if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n",
       "  });\n",
       "});\n"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import bnn"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. The Cifar-10 testset\n",
    "\n",
    "This notebook required the testset from https://www.cs.toronto.edu/~kriz/cifar.html which contains 10000 images that can be processed by CNV network directly without preprocessing.\n",
    "\n",
    "You can download the cifar-10 set from given url via wget and unzip it to a folder on Pynq as shown below.\n",
    "This may take a while as the training set is included in the archive as well.\n",
    "After that we need to read the labels from the binary file to be able to compare the results later:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "#get\n",
    "!wget https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz\n",
    "#unzip\n",
    "!tar -xf cifar-10-binary.tar.gz\n",
    "\n",
    "labels = []\n",
    "with open(\"/home/xilinx/jupyter_notebooks/bnn/cifar-10-batches-bin/test_batch.bin\", \"rb\") as file:\n",
    "    #for 10000 pictures\n",
    "    for i in range(10000):\n",
    "        #read first byte -> label\n",
    "        labels.append(int.from_bytes(file.read(1), byteorder=\"big\"))\n",
    "        #read image (3072 bytes) and do nothing with it\n",
    "        file.read(3072)\n",
    "    file.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Start inference\n",
    "\n",
    "The inference can be performed with different precision for weights and activation. Creating a specific Classifier will automatically download the correct bitstream onto PL and load the weights and thresholds trained on the specific dataset. \n",
    "\n",
    "Thus that images are already Cifar-10 preformatted no preprocessing is required. Therefor the functions `classify_cifar` or `classify_cifars` can be used. When classifying non Cifar-10 formatted pictures refer to `classify_image` or `classify_images`  (see Notebook CNV-QNN_Cifar10).\n",
    "\n",
    "### Case 1: \n",
    "#### W1A1 - 1 bit weight and 1 activation\n",
    "\n",
    "Instantiate the classifier:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "hw_classifier = bnn.CnvClassifier(bnn.NETWORK_CNVW1A1,'cifar10',bnn.RUNTIME_HW)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And start the inference on Cifar-10 preformatted multiple images:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Inference took 1092898.03 microseconds, 109.29 usec per image\n",
      "Classification rate: 9149.98 images per second\n"
     ]
    }
   ],
   "source": [
    "result_W1A1 = hw_classifier.classify_cifars(\"/home/xilinx/jupyter_notebooks/bnn/cifar-10-batches-bin/test_batch.bin\")\n",
    "time_W1A1 = hw_classifier.usecPerImage"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Case 2:\n",
    "#### W1A2 - 1 bit weight and 2 activation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "hw_classifier = bnn.CnvClassifier(bnn.NETWORK_CNVW1A2,'cifar10',bnn.RUNTIME_HW)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Inference took 1092836.00 microseconds, 109.28 usec per image\n",
      "Classification rate: 9150.50 images per second\n"
     ]
    }
   ],
   "source": [
    "result_W1A2 = hw_classifier.classify_cifars(\"/home/xilinx/jupyter_notebooks/bnn/cifar-10-batches-bin/test_batch.bin\")\n",
    "time_W1A2 = hw_classifier.usecPerImage"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Case 3:\n",
    "#### W2A2 - 2 bit weight and 2 activation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "hw_classifier = bnn.CnvClassifier(bnn.NETWORK_CNVW2A2,'cifar10',bnn.RUNTIME_HW)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Inference took 3875513.92 microseconds, 387.55 usec per image\n",
      "Classification rate: 2580.30 images per second\n"
     ]
    }
   ],
   "source": [
    "result_W2A2 = hw_classifier.classify_cifars(\"/home/xilinx/jupyter_notebooks/bnn/cifar-10-batches-bin/test_batch.bin\")\n",
    "time_W2A2 = hw_classifier.usecPerImage"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Summary"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Inference time\n",
    "\n",
    "Results can be visualized using `matplotlib`. Here the comparison of hardware execution time is plotted in microseconds per Image:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXoAAAD8CAYAAAB5Pm/hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDIuMS4xLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvAOZPmwAAEUtJREFUeJzt3X+MZWV9x/H3R0C0trogI93url2i21Zo42KnlErSWDAV0HZpUhRilBqatQkmGk0r2ibapKTYqhiTlmYV6mKsiL/CVqmWIqb1B+CA6wqiYYvUHXfDjiIIMWLBb/+4z7a36+zcOz/uDj68X8nNPec5zzn3e/NMPvfMM+fMTVUhSerXE1a7AEnSZBn0ktQ5g16SOmfQS1LnDHpJ6pxBL0mdM+glqXMGvSR1zqCXpM4dOW7HJEcAM8C3q+olSU4ArgaOBW4DXlFVP0pyNHAV8OvAd4GXVdU9Cx37uOOOq40bNy7tHUjS49Stt976naqaGtVv7KAHXgvcCTy1rb8NuKyqrk7yD8CFwOXt+XtV9ewk57V+L1vowBs3bmRmZmYRpUiSkvzXOP3GmrpJsh54MfDeth7gdOAjrct24Jy2vKWt07af0fpLklbBuHP07wL+DPhxW386cH9VPdLWZ4F1bXkdsAegbX+g9ZckrYKRQZ/kJcD+qrp1uHmerjXGtuHjbk0yk2Rmbm5urGIlSYs3zhn9acDvJ7mHwR9fT2dwhr8myYE5/vXA3rY8C2wAaNufBtx38EGraltVTVfV9NTUyL8lSJKWaGTQV9Wbqmp9VW0EzgM+U1UvB24E/rB1uwC4ti3vaOu07Z8p/+m9JK2a5VxH/0bg9Ul2M5iDv6K1XwE8vbW/Hrh4eSVKkpZjMZdXUlWfBT7blu8GTpmnzw+Bc1egNknSCvDOWEnqnEEvSZ1b1NSNJE3Sxos/udolHHb3XPriib+GZ/SS1DmDXpI6Z9BLUucMeknqnEEvSZ0z6CWpcwa9JHXOoJekzhn0ktQ5g16SOmfQS1LnDHpJ6pxBL0mdM+glqXMGvSR1bmTQJ3lSkluSfCXJHUn+srW/L8k3k+xsj82tPUnenWR3kl1JnjfpNyFJOrRxvnjkYeD0qnooyVHA55L8S9v2p1X1kYP6nwVsao/fBC5vz5KkVTDyjL4GHmqrR7VHLbDLFuCqtt9NwJoka5dfqiRpKcaao09yRJKdwH7g+qq6uW26pE3PXJbk6Na2DtgztPtsa5MkrYKxgr6qHq2qzcB64JQkvwq8CfgV4DeAY4E3tu6Z7xAHNyTZmmQmyczc3NySipckjbaoq26q6n7gs8CZVbWvTc88DPwjcErrNgtsGNptPbB3nmNtq6rpqpqemppaUvGSpNHGuepmKsmatvxk4IXA1w/MuycJcA5we9tlB/DKdvXNqcADVbVvItVLkkYa56qbtcD2JEcw+GC4pqo+keQzSaYYTNXsBP6k9b8OOBvYDfwAeNXKly1JGtfIoK+qXcDJ87Sffoj+BVy0/NIkSSvBO2MlqXMGvSR1zqCXpM4Z9JLUOYNekjpn0EtS5wx6SeqcQS9JnTPoJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ0z6CWpcwa9JHVunC8Hf1KSW5J8JckdSf6ytZ+Q5OYkdyX5UJIntvaj2/rutn3jZN+CJGkh45zRPwycXlXPBTYDZyY5FXgbcFlVbQK+B1zY+l8IfK+qng1c1vpJklbJyKCvgYfa6lHtUcDpwEda+3bgnLa8pa3Ttp+RJCtWsSRpUcaao09yRJKdwH7geuA/gfur6pHWZRZY15bXAXsA2vYHgKfPc8ytSWaSzMzNzS3vXUiSDmmsoK+qR6tqM7AeOAV4znzd2vN8Z+/1Ew1V26pquqqmp6amxq1XkrRIi7rqpqruBz4LnAqsSXJk27Qe2NuWZ4ENAG3704D7VqJYSdLijXPVzVSSNW35ycALgTuBG4E/bN0uAK5tyzvaOm37Z6rqJ87oJUmHx5Gju7AW2J7kCAYfDNdU1SeSfA24OslfAV8Grmj9rwDen2Q3gzP58yZQtyRpTCODvqp2ASfP0343g/n6g9t/CJy7ItVJkpbNO2MlqXMGvSR1zqCXpM4Z9JLUOYNekjpn0EtS5wx6SeqcQS9JnTPoJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ0z6CWpcwa9JHVunC8H35DkxiR3JrkjyWtb+1uTfDvJzvY4e2ifNyXZneQbSV40yTcgSVrYOF8O/gjwhqq6LcnPAbcmub5tu6yq3j7cOcmJDL4Q/CTgF4B/S/JLVfXoShYuSRrPyDP6qtpXVbe15QeBO4F1C+yyBbi6qh6uqm8Cu5nnS8QlSYfHoubok2wETgZubk2vSbIryZVJjmlt64A9Q7vNMs8HQ5KtSWaSzMzNzS26cEnSeMYO+iQ/C3wUeF1VfR+4HHgWsBnYB7zjQNd5dq+faKjaVlXTVTU9NTW16MIlSeMZK+iTHMUg5D9QVR8DqKp7q+rRqvox8B7+b3pmFtgwtPt6YO/KlSxJWoxxrroJcAVwZ1W9c6h97VC3PwBub8s7gPOSHJ3kBGATcMvKlSxJWoxxrro5DXgF8NUkO1vbm4Hzk2xmMC1zD/BqgKq6I8k1wNcYXLFzkVfcSNLqGRn0VfU55p93v26BfS4BLllGXZKkFeKdsZLUOYNekjpn0EtS5wx6SeqcQS9JnTPoJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ0z6CWpcwa9JHXOoJekzhn0ktS5cb4zdkOSG5PcmeSOJK9t7ccmuT7JXe35mNaeJO9OsjvJriTPm/SbkCQd2jhn9I8Ab6iq5wCnAhclORG4GLihqjYBN7R1gLMYfCH4JmArcPmKVy1JGtvIoK+qfVV1W1t+ELgTWAdsAba3btuBc9ryFuCqGrgJWJNk7YpXLkkay6Lm6JNsBE4GbgaOr6p9MPgwAJ7Ruq0D9gztNtvaJEmrYOygT/KzwEeB11XV9xfqOk9bzXO8rUlmkszMzc2NW4YkaZHGCvokRzEI+Q9U1cda870HpmTa8/7WPgtsGNp9PbD34GNW1baqmq6q6ampqaXWL0kaYZyrbgJcAdxZVe8c2rQDuKAtXwBcO9T+ynb1zanAAwemeCRJh9+RY/Q5DXgF8NUkO1vbm4FLgWuSXAh8Czi3bbsOOBvYDfwAeNWKVixJWpSRQV9Vn2P+eXeAM+bpX8BFy6xLkrRCvDNWkjpn0EtS5wx6SeqcQS9JnTPoJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ0z6CWpcwa9JHXOoJekzhn0ktQ5g16SOmfQS1Lnxvly8CuT7E9y+1DbW5N8O8nO9jh7aNubkuxO8o0kL5pU4ZKk8YxzRv8+4Mx52i+rqs3tcR1AkhOB84CT2j5/n+SIlSpWkrR4I4O+qv4duG/M420Brq6qh6vqm8Bu4JRl1CdJWqblzNG/JsmuNrVzTGtbB+wZ6jPb2iRJq2SpQX858CxgM7APeEdrzzx9a74DJNmaZCbJzNzc3BLLkCSNsqSgr6p7q+rRqvox8B7+b3pmFtgw1HU9sPcQx9hWVdNVNT01NbWUMiRJY1hS0CdZO7T6B8CBK3J2AOclOTrJCcAm4JbllShJWo4jR3VI8kHgBcBxSWaBtwAvSLKZwbTMPcCrAarqjiTXAF8DHgEuqqpHJ1O6JGkcI4O+qs6fp/mKBfpfAlyynKIkSSvHO2MlqXMGvSR1zqCXpM4Z9JLUOYNekjpn0EtS5wx6SeqcQS9JnTPoJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ0b+cUjj3UbL/7kapdw2N1z6YtXu4TDznGWls4zeknq3MigT3Jlkv1Jbh9qOzbJ9Unuas/HtPYkeXeS3Ul2JXneJIuXJI02zhn9+4AzD2q7GLihqjYBN7R1gLOATe2xFbh8ZcqUJC3VyKCvqn8H7juoeQuwvS1vB84Zar+qBm4C1iRZu1LFSpIWb6lz9MdX1T6A9vyM1r4O2DPUb7a1/YQkW5PMJJmZm5tbYhmSpFFW+o+xmaet5utYVduqarqqpqempla4DEnSAUsN+nsPTMm05/2tfRbYMNRvPbB36eVJkpZrqUG/A7igLV8AXDvU/sp29c2pwAMHpngkSatj5A1TST4IvAA4Lsks8BbgUuCaJBcC3wLObd2vA84GdgM/AF41gZolSYswMuir6vxDbDpjnr4FXLTcoiRJK8c7YyWpcwa9JHXOoJekzhn0ktQ5g16SOmfQS1LnDHpJ6pxBL0mdM+glqXMGvSR1zqCXpM4Z9JLUOYNekjpn0EtS5wx6SeqcQS9JnTPoJalzI79haiFJ7gEeBB4FHqmq6STHAh8CNgL3AC+tqu8tr0xJ0lKtxBn971TV5qqabusXAzdU1SbghrYuSVolk5i62QJsb8vbgXMm8BqSpDEtN+gL+NcktybZ2tqOr6p9AO35Gct8DUnSMixrjh44rar2JnkGcH2Sr4+7Y/tg2ArwzGc+c5llSJIOZVln9FW1tz3vBz4OnALcm2QtQHvef4h9t1XVdFVNT01NLacMSdIClhz0SZ6S5OcOLAO/C9wO7AAuaN0uAK5dbpGSpKVbztTN8cDHkxw4zj9V1aeSfAm4JsmFwLeAc5dfpiRpqZYc9FV1N/Dcedq/C5yxnKIkSSvHO2MlqXMGvSR1zqCXpM4Z9JLUOYNekjpn0EtS5wx6SeqcQS9JnTPoJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ0z6CWpcwa9JHVuYkGf5Mwk30iyO8nFk3odSdLCJhL0SY4A/g44CzgROD/JiZN4LUnSwiZ1Rn8KsLuq7q6qHwFXA1sm9FqSpAVMKujXAXuG1mdbmyTpMDtyQsfNPG31/zokW4GtbfWhJN+YUC2TdBzwncP9onnb4X7Fxz3HuX+rMsaw7HH+xXE6TSroZ4ENQ+vrgb3DHapqG7BtQq9/WCSZqarp1a5Dk+U496/3MZ7U1M2XgE1JTkjyROA8YMeEXkuStICJnNFX1SNJXgN8GjgCuLKq7pjEa0mSFjapqRuq6jrgukkd/zHip3rqSWNznPvX9Rinqkb3kiT91PJfIEhS5x73QZ/ksiSvG1r/dJL3Dq2/I8nrk3wqyf1JPjHPMaaS/HeSVx/UfkmSPUkemuy70CiTGuckP5Pkk0m+nuSOJJdO/t3oUBYxzl9s47UrycsOOkZ34/y4D3rgC8DzAZI8gcH1tCcNbX8+8Hngb4FXHOIY5wI3Aecf1P7PDO4S1uqb5Di/vap+BTgZOC3JWStYtxZn3HF+ZVWdBJwJvCvJmqE+3Y2zQT8Y9Oe35ZOA24EHkxyT5GjgOcCXq+oG4MFDHON84A3A+iT/ewdwVd1UVfsmV7oWYSLjXFU/qKob2/KPgNsY3Dei1THuON8FUFV7gf3A1NAxuhvnx33Qt4F+JMkzGfyAfBG4GfgtYBrY1QZ2Xkk2AD9fVbcA1wAvO1RfrZ7DMc7trPD3gBtW/h1oHIsd5ySnAE8E/rOtdznOj/ugbw6cBRz4wfji0PoXRux7HoMfCBj887aDf93TY8fExjnJkcAHgXdX1d0rWLMWb6xxTrIWeD/wqqr6cWvucpwndh39T5kD83q/xuBXvT0MfnX7PnDliH3PB45P8vK2/gtJNh341VCPKZMc523AXVX1rpUvW4s0cpyTPBX4JPAXVXXT0L5djrNn9AOfB14C3FdVj1bVfcAaBr/uffFQOyX5ZeApVbWuqjZW1UbgrxmcFeixZyLjnOSvgKcBrzvUMXRYLTjO7d+yfBy4qqo+fGCnnsfZoB/4KoO/zt90UNsDVfUdgCT/AXwYOCPJbJIXMfj0//hBx/poayfJ3ySZBX6m7fPWyb4NjbDi45xkPfDnDL5g57YkO5P88YTfhxY2apxfCvw28EdtvHYm2UzH4+ydsZLUOc/oJalzBr0kdc6gl6TOGfSS1DmDXpI6Z9BLUucMeknqnEEvSZ37HylePfm8gXCXAAAAAElFTkSuQmCC\n",
      "text/plain": [
       "<matplotlib.figure.Figure at 0x7f788081d0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "height = [time_W1A1, time_W1A2, time_W2A2]\n",
    "bars   = ('W1A1', 'W1A2', 'W2A2')\n",
    "\n",
    "y_pos=range(3)\n",
    "plt.bar(y_pos, height, 0.5)\n",
    "plt.xticks(y_pos, bars)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Accuracy\n",
    "\n",
    "The accuracy on the testset can be calculated by comparing the inferred labels against the one read at the beginning:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Accuracy W1A1:  79.22 %\n",
      "Accuracy W1A2:  82.66 %\n",
      "Accuracy W2A2:  84.29 %\n"
     ]
    }
   ],
   "source": [
    "#compare against labels\n",
    "countRight = 0\n",
    "for idx in range(len(labels)):\n",
    "    if labels[idx] == result_W1A1[idx]:\n",
    "        countRight += 1\n",
    "accuracyW1A1 = countRight*100/len(labels)\n",
    "\n",
    "countRight = 0\n",
    "for idx in range(len(labels)):\n",
    "    if labels[idx] == result_W1A2[idx]:\n",
    "        countRight += 1\n",
    "accuracyW1A2 = countRight*100/len(labels)\n",
    "\n",
    "countRight = 0\n",
    "for idx in range(len(labels)):\n",
    "    if labels[idx] == result_W2A2[idx]:\n",
    "        countRight += 1\n",
    "accuracyW2A2 = countRight*100/len(labels)\n",
    "\n",
    "print(\"Accuracy W1A1: \",accuracyW1A1,\"%\")\n",
    "print(\"Accuracy W1A2: \",accuracyW1A2,\"%\")\n",
    "print(\"Accuracy W2A2: \",accuracyW2A2,\"%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Reset the device"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pynq import Xlnk\n",
    "\n",
    "xlnk = Xlnk()\n",
    "xlnk.xlnk_reset()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
